aboutsummaryrefslogtreecommitdiff
path: root/frontend/src/routes/services/[id]
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/src/routes/services/[id]')
-rw-r--r--frontend/src/routes/services/[id]/+page.svelte261
1 files changed, 261 insertions, 0 deletions
diff --git a/frontend/src/routes/services/[id]/+page.svelte b/frontend/src/routes/services/[id]/+page.svelte
new file mode 100644
index 0000000..36ea105
--- /dev/null
+++ b/frontend/src/routes/services/[id]/+page.svelte
@@ -0,0 +1,261 @@
+<script lang="ts">
+ import { onMount } from 'svelte';
+ import { page } from '$app/stores';
+ import { getHistory, getServiceStats, listServices } from '$lib/api';
+ import { subscribeHeartbeats } from '$lib/realtime';
+ import type { Service, Heartbeat, ServiceStats } from '$lib/types';
+
+ let svc: Service | undefined = $state();
+ let history: Heartbeat[] = $state([]);
+ let stats: ServiceStats | null = $state(null);
+ let loading = $state(true);
+ let expandedError: number | null = $state(null);
+
+ onMount(async () => {
+ try {
+ const all = await listServices();
+ svc = all.find((s) => s.id === Number($page.params.id));
+ history = await getHistory(Number($page.params.id));
+ getServiceStats(Number($page.params.id)).then((s) => (stats = s)).catch(() => {});
+ } catch {
+ // handle
+ } finally {
+ loading = false;
+ }
+ });
+
+ $effect(() => {
+ if (!svc && $page.params.id) {
+ getHistory(Number($page.params.id))
+ .then((h) => (history = h))
+ .catch(() => {});
+ }
+ });
+
+ onMount(() => {
+ const unsub = subscribeHeartbeats((hb) => {
+ if (hb.service_id !== Number($page.params.id)) return;
+ history = [...history, hb].slice(-50);
+ });
+ return unsub;
+ });
+
+ let chartPad = { t: 8, b: 20, l: 40, r: 12 };
+ let chartW = $derived(Math.max(history.length * 32, 300));
+ let chartH = 200;
+ let plotW = $derived(chartW - chartPad.l - chartPad.r);
+ let plotH = $derived(chartH - chartPad.t - chartPad.b);
+ let chartMax = $derived(Math.max(...history.map((h) => h.response_time_ms), 100));
+ let chartMin = $derived(0);
+
+ let dots = $derived(
+ history.map((h, i) => {
+ const x = history.length === 1
+ ? chartPad.l + plotW / 2
+ : chartPad.l + (i / (history.length - 1)) * plotW;
+ const y = chartPad.t + plotH - ((h.response_time_ms - chartMin) / (chartMax - chartMin || 1)) * plotH;
+ return { x, y, ...h };
+ })
+ );
+
+ let yTicks = $derived.by(() => {
+ const n = 4;
+ return Array.from({ length: n + 1 }, (_, i) => {
+ const val = chartMin + ((chartMax - chartMin) / n) * i;
+ const y = chartPad.t + plotH - ((val - chartMin) / (chartMax - chartMin || 1)) * plotH;
+ return { val: Math.round(val), y };
+ });
+ });
+</script>
+
+<svelte:head>
+ <title>{svc?.name ?? 'Detalhes'} — YAUM</title>
+</svelte:head>
+
+{#if loading}
+ <div class="flex items-center justify-center py-20">
+ <div
+ class="h-8 w-8 animate-spin rounded-full border-2 border-[var(--border-color)] border-t-[var(--green)]"
+ ></div>
+ </div>
+{:else if !svc}
+ <div class="rounded-xl border border-[var(--border-color)] p-12 text-center">
+ <p class="text-sm text-[var(--text-muted)]">Serviço não encontrado</p>
+ </div>
+{:else}
+ <div class="mb-6">
+ <a
+ href="/"
+ class="text-xs text-[var(--text-muted)] underline transition-colors hover:text-white"
+ >
+ ← Voltar
+ </a>
+ </div>
+
+ <div class="mb-8">
+ <h1 class="text-2xl font-semibold text-white">{svc.name}</h1>
+ <p class="mt-1 font-mono text-sm text-[var(--text-secondary)]">{svc.url}</p>
+ </div>
+
+ <div class="grid gap-6 lg:grid-cols-3">
+ <!-- Stats card -->
+ {#if stats}
+ <div class="rounded-xl border border-[var(--border-color)] bg-[var(--bg-card)] p-5">
+ <h2 class="mb-4 text-sm font-semibold uppercase tracking-wider text-[var(--text-secondary)]">
+ Uptime
+ </h2>
+ <div class="space-y-3">
+ {#each [
+ { label: '24h', uptime: stats.uptime_24h, checks: stats.total_checks_24h, avg: stats.avg_response_ms_24h },
+ { label: '7 dias', uptime: stats.uptime_7d, checks: stats.total_checks_7d, avg: stats.avg_response_ms_7d },
+ { label: '30 dias', uptime: stats.uptime_30d, checks: stats.total_checks_30d, avg: stats.avg_response_ms_30d }
+ ] as item}
+ <div class="rounded-lg border border-[var(--border-color)] bg-[var(--bg-secondary)]/30 p-3">
+ <div class="mb-1 flex items-center justify-between">
+ <span class="text-[10px] font-medium uppercase tracking-wider text-[var(--text-muted)]">
+ {item.label}
+ </span>
+ <span class="font-mono text-xs tabular-nums text-[var(--text-secondary)]">
+ {item.checks} checks
+ </span>
+ </div>
+ <div class="flex items-baseline gap-3">
+ <span
+ class="text-lg font-bold tabular-nums"
+ style="color: {item.uptime >= 99 ? 'var(--green)' : item.uptime >= 95 ? '#facc15' : 'var(--red)'}"
+ >
+ {item.uptime.toFixed(2)}%
+ </span>
+ <span class="font-mono text-xs text-[var(--text-muted)]">
+ {item.avg.toFixed(0)}ms méd.
+ </span>
+ </div>
+ </div>
+ {/each}
+ </div>
+ </div>
+ {/if}
+
+ <!-- Tempo de Resposta (ms) -->
+ <div
+ class="rounded-xl border border-[var(--border-color)] bg-[var(--bg-card)] p-5 lg:col-span-2"
+ >
+ <h2 class="mb-4 text-sm font-semibold uppercase tracking-wider text-[var(--text-secondary)]">
+ Tempo de Resposta (ms)
+ </h2>
+ <div class="relative h-48">
+ {#if history.length === 0}
+ <div class="flex h-full items-center justify-center text-xs text-[var(--text-muted)]">
+ Sem dados ainda
+ </div>
+ {:else if history.length === 1}
+ <div class="flex h-full flex-col items-center justify-center gap-1">
+ <span
+ class="text-3xl font-bold tabular-nums"
+ style="color: {history[0].is_up ? 'var(--green)' : 'var(--red)'}"
+ >
+ {history[0].response_time_ms}
+ <span class="text-base font-normal text-[var(--text-secondary)]">ms</span>
+ </span>
+ <span class="text-xs text-[var(--text-muted)]">
+ {history[0].is_up ? 'Online' : 'Offline'} —
+ {history[0].status_code || 'timeout'}
+ </span>
+ </div>
+ {:else}
+ <svg class="h-full w-full" viewBox="0 0 {chartW} {chartH}" preserveAspectRatio="none">
+ {#each yTicks as tick}
+ <line
+ x1={chartPad.l}
+ y1={tick.y}
+ x2={chartW - chartPad.r}
+ y2={tick.y}
+ stroke="var(--border-color)"
+ stroke-width="1"
+ stroke-dasharray="3,3"
+ />
+ <text
+ x={chartPad.l - 6}
+ y={tick.y + 3}
+ text-anchor="end"
+ fill="var(--text-muted)"
+ font-size="9"
+ >{tick.val}</text
+ >
+ {/each}
+
+ <polyline
+ points={dots.map((d) => `${d.x},${d.y}`).join(' ')}
+ fill="none"
+ stroke="var(--green)"
+ stroke-width="2"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+
+ {#each dots as d}
+ <circle cx={d.x} cy={d.y} r="3.5" fill={d.is_up ? 'var(--green)' : 'var(--red)'} />
+ {/each}
+ </svg>
+ {/if}
+ </div>
+ <div class="mt-2 flex justify-between text-[10px] text-[var(--text-muted)]">
+ <span>{history[0]?.tested_at ? new Date(history[0].tested_at).toLocaleTimeString() : ''}</span>
+ <span
+ >{history[history.length - 1]?.tested_at
+ ? new Date(history[history.length - 1].tested_at).toLocaleTimeString()
+ : ''}</span
+ >
+ </div>
+ </div>
+
+ <!-- Timeline compacta -->
+ <div class="rounded-xl border border-[var(--border-color)] bg-[var(--bg-card)] p-5">
+ <h2 class="mb-4 text-sm font-semibold uppercase tracking-wider text-[var(--text-secondary)]">
+ Últimas Verificações
+ </h2>
+ <div class="flex flex-col gap-1.5">
+ {#each history.slice().reverse() as h}
+ <div>
+ <div class="flex items-center justify-between gap-2">
+ <div class="flex items-center gap-2">
+ <span
+ class="h-2 w-2 shrink-0 rounded-full"
+ class:bg-[var(--green)]={h.is_up}
+ class:bg-[var(--red)]={!h.is_up}
+ ></span>
+ <span class="font-mono text-xs">
+ {#if h.status_code > 0}
+ {h.status_code}
+ {:else}
+ TIMEOUT
+ {/if}
+ </span>
+ </div>
+ <div class="flex items-center gap-2">
+ <span class="font-mono text-xs text-[var(--text-muted)]">{h.response_time_ms}ms</span>
+ {#if h.error_message}
+ <button
+ onclick={() => (expandedError = expandedError === h.id ? null : h.id)}
+ class="text-[10px] text-[var(--red)] underline underline-offset-2 transition-colors hover:opacity-70"
+ >
+ {expandedError === h.id ? '▲ erro' : '▼ erro'}
+ </button>
+ {/if}
+ </div>
+ </div>
+ {#if expandedError === h.id && h.error_message}
+ <div
+ class="mt-1.5 overflow-auto rounded-md border border-[var(--red)]/20 bg-[var(--bg-secondary)]/50 p-2"
+ >
+ <pre class="break-all font-mono text-[11px] leading-relaxed text-[var(--text-muted)]"
+ >{h.error_message}</pre
+ >
+ </div>
+ {/if}
+ </div>
+ {/each}
+ </div>
+ </div>
+ </div>
+{/if}